# Copyright (c) 2018 Presto Labs Pte. Ltd.
# Author: xguo

import collections
import enum
import functools
import json
import logging
import math
import typing

import requests
import tornado.gen

import coin.flow.topic_map as topic_map
import coin.flow.subscriber as flow_subscriber
import coin.proto.coin_market_enums_pb2 as coin_enum

from coin.base.config import Config
from coin.base.timestamp import get_timestamp
from coin.exchange.base.order_fill_util import FillManager, LastFill
from coin.exchange.base.order_gateway_ready import SpotOrderGatewayReady
from coin.exchange.base.rest.task_scheduler import Scheduler, wrap_async_task
from coin.exchange.huobi.kr_rest.currency import HuobiCurrency
from coin.exchange.huobi.kr_rest.private_client import HuobiPrivateParser
from coin.exchange.huobi.kr_rest.product import HuobiProduct
from coin.exchange.huobi.ws.ws_private_client import HuobiWsPrivateClient
from coin.exchange.util.rest_rate_limit import RestRateLimiter
from coin.proto.coin_order_gateway_pb2 import OrderGatewayConfig, OrderEvent
from coin.proto.coin_query_pb2 import AccountBalance
from coin.strategy.hk_hard_arb.og_util import print_working_orders

from coin.exchange.huobi.kr_rest.native_private_client import (
    HuobiAccountType,
    HuobiNativePrivateClient,
    HuobiOrderState,
    HuobiOrderType,
)
from coin.exchange.base.order_gateway import (
    BaseOrderGatewayInfoMutator,
    Order,
    OrderGatewayStatus,
    OrderSide,
    OrderType,
)
from coin.exchange.base.rest_order_gateway_base import (
    PrivateExchangeMessage,
    RestOrderGateway,
    coin_order_pb2,
    convert_to_requests_response,
    create_order_internal,
    exchange_message_handler,
)


def to_huobi_order_type(order: Order, post_only):
  assert order.order_side in (OrderSide.BUY, OrderSide.SELL), order
  assert order.order_type == OrderType.LIMIT, order

  order_side = order.order_side.name.lower()
  order_type = order.order_type.name.lower()
  if post_only:
    huobi_type = '-'.join([order_side, order_type, 'maker'])
  else:
    huobi_type = '-'.join([order_side, order_type])
  return HuobiOrderType(huobi_type)


@functools.lru_cache(maxsize=64)
def from_huobi_order_type(huobi_order_type: HuobiOrderType):
  fields = ('order_side', 'order_type', 'maker')
  order_side_map = {
      'sell': OrderSide.SELL,
      'buy': OrderSide.BUY,
  }
  order_type_map = {
      'limit': OrderType.LIMIT,
      'market': OrderType.MARKET,
      'ioc': OrderType.IOC,
  }
  parsed_result = dict(zip(fields, huobi_order_type.value.split('-')))
  return {
      'order_side': order_side_map[parsed_result['order_side']],
      'order_type': order_type_map[parsed_result['order_type']],
      'post_only': ('maker' in parsed_result),
  }


def create_order_from_open_order_message(message, timestamp, order_id=None):
  """
  open_orders:
  {
  "seq-id": 100756827765,
  "order-id": 28481252760,
  "symbol": "btcusdt",
  "account-id": 4836027,
  "order-amount": "0.34",
  "order-price": "4923.53",
  "created-at": 1554288859279,
  "order-type": "sell-limit-maker",
  "order-source": "spot-api",
  "order-state": "canceled",
  "role": "taker",
  "price": "4923.53",
  "filled-amount": "0",
  "unfilled-amount": "0.34",
  "filled-cash-amount": "0",
  "filled-fees": "0",
  "id": 28481252760,
  "amount": "0.34",
  "type": "sell-limit-maker"
  }

  order detail:
  {
    "status": "ok",
    "data": {
    "id": 28478169694,
    "symbol": "btcusdt",
    "account-id": 4836027,
    "amount": "0.340000000000000000",
    "price": "4929.940000000000000000",
    "created-at": 1554286093888,
    "type": "buy-limit-maker",
    "field-amount": "0.0",
    "field-cash-amount": "0.0",
    "field-fees": "0.0",
    "finished-at": 0,
    "source": "spot-api",
    "state": "submitted",
    "canceled-at": 1554286094124
    }
  }
  """
  logger = logging.getLogger(__name__)
  exchange_order_id = str(message['id'])
  price_orig = float(message['price'])
  qty_orig = float(message['amount'])
  parsed_type_result = from_huobi_order_type(HuobiOrderType(message['type']))
  product = HuobiProduct.FromStrNativeProduct(message['symbol'])
  status = HuobiOrderState(message['state'])

  amount_last_fill = float(message['filled-cash-amount'])
  qty_last_fill = float(message['filled-amount'])
  if qty_last_fill > 0:
    price_last_fill = amount_last_fill / qty_last_fill
    if not math.isclose(price_last_fill, price_orig):
      logger.info('price_last_fill: %s, price_orig: %s', price_last_fill, price_orig)
  else:
    price_last_fill = 0

  fill = FillManager(
      price_orig=price_orig,
      qty_orig=qty_orig,
      price_last_fill=price_last_fill,
      qty_last_fill=qty_last_fill,
      price_avg_fill=0,
      qty_accum_fill=0,
      timestamp_last_fill=timestamp,
  )

  order = Order(order_id=order_id,
                product=product,
                price=price_orig,
                qty=qty_orig,
                order_side=parsed_type_result['order_side'],
                order_type=parsed_type_result['order_type'],
                tag=None,
                accepted=True,
                cancel_sent=False,
                internal=create_order_internal(
                    timestamp=timestamp,
                    exchange_order_id=exchange_order_id,
                    fill=fill,
                    status=status,
                ))
  return order


class HuobiOrderGatewayInfo(BaseOrderGatewayInfoMutator):
  CurrencyType = HuobiCurrency
  ProductType = HuobiProduct
  BalanceType = float
  PositionType = None

  def update_balance(self, message):
    msg = HuobiPrivateParser.parse_balance(message)
    for balance in msg.each_balance:
      currency = HuobiCurrency.FromStrNativeCurrency(balance.currency_native)
      if currency in self.currencies:
        self.set_balance(currency, balance.total, balance)

  def balance_as_proto(self):
    return AccountBalance(exchange='Huobi', each_balance=self.get_notNone_balances_internal())


def _get_huobi_rest_match_key(match_info):
  exchange_order_id = str(match_info['order-id'])
  trade_id = str(match_info['trade-id'])
  native_symbol = match_info['symbol']
  key = (exchange_order_id, trade_id, native_symbol)
  return key


class HuobiOrderGateway(RestOrderGateway):
  _exchange = 'Huobi'
  _market_type = 'Spot'
  _api_version = None

  OGInfoType = HuobiOrderGatewayInfo

  class _MsgType(enum.IntEnum):
    REST_ACCOUNT_BALANCE = 0
    WS_ACCOUNT_BALANCE = 1
    OPEN_ORDERS = 2
    ORDER_UPDATE = 3
    MATCH_RESULTS = 4
    FIRST_MATCH_RESULTS = 5
    ORDER_DETAIL = 6

  def __init__(self,
               currencies: typing.List[HuobiCurrency],
               products: typing.List[HuobiProduct],
               config: Config,
               og_config=None,
               logger=None):
    super().__init__(currencies, products, config, og_config, logger)
    self._og_ready = SpotOrderGatewayReady()
    self._og_ready.set_status(OrderGatewayStatus.INITIALIZING)

    self._account_id = None
    self._account_type = HuobiAccountType.SPOT

    # Following instance attributes are initialized in start method.
    self._flow_sub = None
    self._ioloop = None
    self._publisher = None
    self._scheduler = None
    self._private_client = None
    self._ws_private_client = None
    self._rest_rate_limiter = RestRateLimiter(delay=10)

    self._og_config = OrderGatewayConfig(
        account_query_period=1,
        orders_snapshot_query_period=1,
        fill_query_period=1,
        rest_lib='pycurl',
    )
    if og_config is not None:
      self._og_config.MergeFrom(og_config)

    self._og_logger.exchange_type = coin_enum.Huobi
    self._rest_fills = collections.defaultdict(FillManager)
    self._ws_fills = collections.defaultdict(FillManager)
    self._processed_match = set()

  def submittable(self):
    return self.is_ready() and self._rest_rate_limiter.is_available()

  def _notification_callback(self, data):
    assert data['action'] == 'push', data
    timestamp = get_timestamp()
    topic = data['ch']
    if topic.startswith('accounts'):
      msg_type = self._MsgType.WS_ACCOUNT_BALANCE
    elif topic.startswith('orders'):
      msg_type = self._MsgType.ORDER_UPDATE
    else:
      self._logger.error('Unknown subscription topic! %s', data)
      return

    exchange_message = PrivateExchangeMessage(msg_type=msg_type,
                                              data=data,
                                              received_timestamp=timestamp,
                                              request_params=None,
                                              request_timestamp=None)
    self._publisher.write(timestamp, exchange_message)

  def start(self, queue, ioloop, flow_sub=None):
    api_host = self._og_config.rest_root_url if \
               self._og_config.HasField('rest_root_url') else None
    disable_ssl = self._og_config.rest_disable_ssl

    topic_id = topic_map.search_topic_id('huobi_rest_private_1')[0][1]
    self._ioloop = ioloop

    self._publisher = queue.get_writer(topic_id=topic_id)
    self._scheduler = Scheduler(ioloop=ioloop, logger=self._logger)

    self._private_client = HuobiNativePrivateClient(key_file=self._config.key_file,
                                                    api_host=api_host,
                                                    disable_ssl=disable_ssl,
                                                    use_async=True,
                                                    rest_lib=self._og_config.rest_lib)
    self._prepare_ws_private_client(start=True)
    self._query_account_id()
    self._install_periodic_query()

    # Subscribe to flow for feed.
    self._flow_sub = flow_sub or flow_subscriber.from_queue(queue)
    self._flow_sub.subscribe('huobi_rest_private_1', self.on_private_exchange_msg)

  def _prepare_ws_private_client(self, start=False):
    if self._ws_private_client is not None:
      self._ws_private_client.stop()

    self._ws_private_client = HuobiWsPrivateClient(
        self._config.key_file,
        ioloop=self._ioloop,
        notification_callback=self._notification_callback,
        close_callback=functools.partial(self._prepare_ws_private_client, True),
        on_ready=self._on_ws_private_client_ready,
    )

    if start:
      self._ws_private_client.start()

  def _on_ws_private_client_ready(self):
    native_symbols = [prod.native_symbol for prod in self._og_info.products]
    self._ws_private_client.prepare_subscription(native_symbols)

  @tornado.gen.coroutine
  def _query_account_id(self):
    response = yield self._private_client.query_accounts()
    response = convert_to_requests_response(response)
    success = False

    if response.status_code == requests.codes.ok:
      message = response.json()
      if message['status'] == 'ok':
        for info in message['data']:
          if info['type'] == self._account_type.value:
            if info['state'] == 'working':
              self._account_id = info['id']
              success = True
              self._logger.info('Huobi account id  (%s): %s',
                                self._account_type.value,
                                self._account_id)
              break
            else:
              self._logger.error('account not working! %s', message)
      else:
        self._logger.error('Status is not ok! %s', message)
    else:
      self._logger.error('Query response error: %s', response.status_code, response.content)

    if not success:
      self._logger.error('Query account id failed, try again!\n%s\n%s',
                         response.status_code,
                         response.content)
      self._ioloop.call_later(5, self._query_account_id)

  @tornado.gen.coroutine
  def _install_periodic_query(self):
    assert self._scheduler is not None, 'self._scheduler is not set!'
    assert self._publisher is not None, 'self._publisher is not set!'

    while self._account_id is None:
      yield tornado.gen.sleep(1)

    query_balance_task = wrap_async_task(self._private_client.query_account_balance,
                                         msg_type=self._MsgType.REST_ACCOUNT_BALANCE,
                                         publisher=self._publisher,
                                         func_params={'account_id': self._account_id})

    # query balance
    self._scheduler.add_periodic_task(period=self._og_config.account_query_period,
                                      job_func=query_balance_task)

    # query open orders
    yield tornado.gen.sleep(self._og_config.burst_query_avoid_time)
    query_orders_task = wrap_async_task(
        self._private_client.query_open_orders,
        msg_type=self._MsgType.OPEN_ORDERS,
        publisher=self._publisher,
    )

    self._scheduler.add_periodic_task(
        period=self._og_config.orders_snapshot_query_period,
        job_func=query_orders_task,
    )

    query_match_result_task = wrap_async_task(
        self._private_client.query_order_match_results,
        msg_type=self._MsgType.FIRST_MATCH_RESULTS,
        publisher=self._publisher,
    )

    self._scheduler.run_once(job_func=query_match_result_task,)

    def print_info():
      print_working_orders(self._logger, self)

    self._scheduler.add_periodic_task(
        period=10,
        job_func=print_info,
    )
    self._scheduler.start()

  def _async_submit_order(self, order, post_only):
    assert order.order_id is not None, order
    assert order.order_type == OrderType.LIMIT, order

    formatter = self._order_formatter[order.product]
    huobi_order_type = to_huobi_order_type(order, post_only)
    params = {
        'account_id': self._account_id,
        'symbol': order.product.native_symbol,
        'price': formatter.format_price(order.price),
        'amount': formatter.format_qty(order.qty),
        'huobi_order_type': huobi_order_type,
    }
    fut = self._private_client.place_order(**params)
    return fut

  def _async_cancel_order(self, order):
    assert order.internal.exchange_order_id is not None, order
    fut = self._private_client.cancel_order(order_id=order.internal.exchange_order_id)
    return fut

  def _handle_submit_response(self, order, response):
    if response.status_code != requests.codes.ok:
      self._order_manager.remove_order(order, ignore_error=True)
      if response.status_code == 429:
        self._rest_rate_limiter.set_error(response.status_code)
        self._logger.error('Rate limit hit!')
      else:
        self._logger.error('Submit order %s failed!\n%s', order, response.content)
      return OrderEvent.ORDER_REJECTED

    message = response.json()
    if message['status'] != 'ok':
      self._logger.error('Status is not ok! %s', message)
      self._order_manager.remove_order(order, ignore_error=True)
      return OrderEvent.ORDER_ERROR

    exchange_order_id = message['data']
    prev_order = self._order_manager.get(exchange_order_id=exchange_order_id)
    if prev_order is None:
      self._order_manager.set_order(order, exchange_order_id=exchange_order_id)
    else:
      if prev_order.order_id != order.order_id:
        self._logger.info('order id replaced: %s => %s', prev_order.order_id, order.order_id)
        self._order_manager.remove_order(prev_order)
        self._order_manager.set_order(order, exchange_order_id=exchange_order_id)
        self._order_manager.remove_order(order)
        prev_order.order_id = order.order_id
        self._order_manager.add_order(prev_order)
    return OrderEvent.ORDER_ACCEPTED

  def _handle_cancel_response(self, order, response):
    if response.status_code != requests.codes.ok:
      self._logger.error('Cancel order failed with http response code=%s!. %s' %
                         (response.status_code, response.content))
      return OrderEvent.CANCEL_ERROR

    message = response.json()
    if message['status'] != 'ok':
      self._logger.error('Cancel order (%s) error! %s', order.order_id, message)
      if self._order_manager.is_dead_order(order):
        self._logger.warning('Order (%s) is dead already!', order.order_id)
      else:
        self._logger.error('Unexpected error! Need inspection! %s', message)
      return OrderEvent.CANCEL_ERROR
    elif message['data'] != order.internal.exchange_order_id:
      self._logger.error('Insane response, exchange order id not consistent: %s, %s',
                         order,
                         message)
      return OrderEvent.CANCEL_ERROR
    else:
      if not self._order_manager.is_dead_order(order):
        task = wrap_async_task(self._private_client.query_single_order,
                               msg_type=self._MsgType.ORDER_DETAIL,
                               publisher=self._publisher,
                               func_params=dict(order_id=order.internal.exchange_order_id))
        self._scheduler.run_once(task, delay=0.5)
      else:
        self._logger.info('Order is dead already. %s', order.order_id)
      return OrderEvent.CANCEL_ACCEPTED

  def _is_processed(self, match_info):
    key = _get_huobi_rest_match_key(match_info)
    return key in self._processed_match

  def _add_to_processed(self, match_info):
    key = _get_huobi_rest_match_key(match_info)
    self._processed_match.add(key)

  def _update_fill_info(self, order, price_last_fill, qty_last_fill, is_rest):
    if is_rest:
      fill_info = self._rest_fills[order.internal.exchange_order_id]
    else:
      fill_info = self._ws_fills[order.internal.exchange_order_id]

    if fill_info.value.qty_orig:
      is_same_qty = math.isclose(fill_info.value.qty_orig, order.qty)
      is_same_price = math.isclose(fill_info.value.price_orig, order.price)
      if not (is_same_qty and is_same_price):
        self._logger.error('Inconsistent fill_info. %s, %s', fill_info, order)
    else:
      fill_info.value.qty_orig = order.qty
      fill_info.value.price_orig = order.price

    last_fill = fill_info.update_by_last_fill(price_last_fill, qty_last_fill)
    if last_fill is None:
      self._logger.error('Invalid fill msg received! %s', order)
    return fill_info

  def _process_rest_fill(self, match_info, order, timestamp):
    if order is None:
      self._logger.error('Order not found %s', match_info)
      return

    price_last_fill = float(match_info['price'])
    qty_last_fill = float(match_info['filled-amount'])
    role = match_info['role']
    if role == 'maker':
      fill_type = coin_order_pb2.MAKER_FILL_TYPE
    elif role == 'taker':
      fill_type = coin_order_pb2.TAKER_FILL_TYPE
    else:
      self._logger.error('Unknown role type! %s', match_info)
      fill_type = None

    fill_info = self._update_fill_info(order, price_last_fill, qty_last_fill, is_rest=True)
    last_fill = order.internal.fill.update_by_accum_fill(fill_info.value.price_avg_fill,
                                                         fill_info.value.qty_accum_fill)
    if last_fill is not None:
      self._logger.info(
          'Rest fill msg come earlier, ws fill msg may missing!, exchange_order_id=%s',
          match_info['order-id'])
      self._process_last_fill(last_fill, order, fill_type, timestamp)

  def _process_last_fill(self, last_fill, order, fill_type, timestamp):
    if last_fill is None:
      return
    self._publish_fill(last_fill.price_last_fill, last_fill.qty_last_fill, order)
    self._og_logger.gen_order_filled_event_and_log(order, timestamp, fill_type=fill_type)
    order.internal.fill.log_last_fill(order, self._logger)

  @exchange_message_handler(_MsgType.OPEN_ORDERS)
  def _handle_open_orders(self, exchange_message: PrivateExchangeMessage):
    timestamp = get_timestamp()
    response = exchange_message.data
    if response.status_code != requests.codes.ok:
      self._logger.error('Rest open orders response error! %s, %s',
                         response.status_code,
                         response.content)
      return

    message = response.json()
    if message['status'] != 'ok':
      self._logger.error('status is not ok: %s' % message)
      return

    exchange_open_order_dict = {}
    for entry in message['data']:
      account_id = entry['account-id']
      if account_id != self._account_id:
        continue  # ignore

      native_symbol = entry['symbol']
      product = HuobiProduct.FromStrNativeProduct(native_symbol)
      if product not in self._og_info.products:
        continue  # ignore

      entry['extra_info'] = 'open_orders'
      order = create_order_from_open_order_message(entry, timestamp)
      exchange_order_id = order.internal.exchange_order_id
      prev_order = self._order_manager.get(exchange_order_id=exchange_order_id)
      if prev_order is None:
        order_id = self._order_id_generator.gen_order_id(is_foreign=True)
        order.order_id = order_id
      else:
        order.order_id = prev_order.order_id
      exchange_open_order_dict[exchange_order_id] = order

    self._order_manager.copy_from_exchange_open_orders(exchange_open_order_dict,
                                                       self._og_info.products)

    if not self.is_ready():
      self._og_ready.set_orders_ready(True)
      self._og_ready.check_ready()

  @exchange_message_handler(_MsgType.REST_ACCOUNT_BALANCE)
  def _handle_rest_balance(self, exchange_message: PrivateExchangeMessage):
    response = exchange_message.data
    if response.status_code != requests.codes.ok:
      self._logger.error('Rest balance response error! %s, %s',
                         response.status_code,
                         response.content)
      return

    message = response.json()
    if message['status'] != 'ok':
      self._logger.error('status is not ok: %s' % message)
      return

    self._og_info.update_balance(message)
    self._og_logger.write_balance(self._og_info.balance_as_proto())
    if not self.is_ready():
      self._og_ready.set_balance_ready(True)
      self._og_ready.check_ready()

  @exchange_message_handler(_MsgType.FIRST_MATCH_RESULTS)
  def _handle_first_matchresults(self, exchange_message: PrivateExchangeMessage):
    response = exchange_message.data
    if response.status_code != requests.codes.ok:
      self._logger.error('Rest balance response error! %s, %s',
                         response.status_code,
                         response.content)
      return

    message = response.json()
    if message['status'] != 'ok':
      self._logger.error('status is not ok: %s' % message)
      return

    for match in message['data']:
      self._add_to_processed(match)

    self._logger.info('Get all match results for the first time!')
    query_match_result_task = wrap_async_task(
        self._private_client.query_order_match_results,
        msg_type=self._MsgType.MATCH_RESULTS,
        publisher=self._publisher,
    )

    self._scheduler.add_periodic_task(
        period=self._og_config.fill_query_period,
        job_func=query_match_result_task,
    )

  @exchange_message_handler(_MsgType.MATCH_RESULTS)
  def _handle_rest_matchresults(self, exchange_message: PrivateExchangeMessage):
    response = exchange_message.data
    if response.status_code != requests.codes.ok:
      self._logger.error('Rest balance response error! %s, %s',
                         response.status_code,
                         response.content)
      return

    message = response.json()
    if message['status'] != 'ok':
      self._logger.error('status is not ok: %s' % message)
      return

    timestamp = get_timestamp()
    for match in message['data']:
      if self._is_processed(match):
        continue

      exchange_order_id = str(match['order-id'])
      native_symbol = match['symbol']
      product = HuobiProduct.FromStrNativeProduct(native_symbol)
      if product not in self._og_info.products:
        self._logger.error('Receive fill message of irrelevant product! %s', native_symbol)
        self._add_to_processed(match)
        continue  # Ignore.

      order = self._order_manager.get(exchange_order_id=exchange_order_id)
      if order is None:
        self._logger.debug('Rest fill comes before submit response! %s', exchange_order_id)
        continue
      self._add_to_processed(match)
      self._process_rest_fill(match, order, timestamp)

  # not using
  @exchange_message_handler(_MsgType.WS_ACCOUNT_BALANCE)
  def _handle_ws_accounts(self, exchange_message: PrivateExchangeMessage):
    message = exchange_message.data
    # Make this message appear to be same as rest balance message
    message['status'] = 'ok'
    info_list = message['data']['list']
    self._logger.debug('Balance change event: %s', message['data']['event'])
    message['data']['list'] = [item for item in info_list if item['account-id'] == self._account_id]
    self._og_info.update_balance(message)
    self._og_logger.write_balance(self._og_info.balance_as_proto())

  @exchange_message_handler(_MsgType.ORDER_UPDATE)
  def _handle_ws_orders(self, exchange_message: PrivateExchangeMessage):
    timestamp = get_timestamp()
    message = exchange_message.data['data']

    native_symbol = message['symbol']
    product = HuobiProduct.FromStrNativeProduct(native_symbol)
    if product not in self._og_info.products:
      self._logger.error('Weird! Not suppose to receive %s order update!', product.full_symbol)
      return

    event_type = message['eventType']
    if event_type == 'creation':
      # Make message similar as rest open orders message
      message['price'] = message['orderPrice']
      message['amount'] = message['orderSize']
      message['id'] = message['orderId']
      message['type'] = message['type']
      message['state'] = message['orderStatus']
      message['filled-cash-amount'] = '0'
      message['filled-amount'] = '0'
      message['extra_info'] = 'ws_order_info'
      order_info = create_order_from_open_order_message(message, timestamp)

      order = self._order_manager.get(exchange_order_id=order_info.internal.exchange_order_id)

      if order is None:
        self._logger.info('Receive foreign order update.')
        order_id = self._order_id_generator.gen_order_id(is_foreign=True)
        order_info.order_id = order_id
        self._order_manager.add_order(order_info)
    elif event_type == 'trade':
      # Process fills
      exchange_order_id = str(message['orderId'])
      order = self._order_manager.get(exchange_order_id=exchange_order_id)
      if order is None:
        self._logger.error('Ws fill can not find order! %s', exchange_order_id)
        return

      message['order-id'] = str(message['orderId'])
      message['trade-id'] = str(message['tradeId'])
      if self._is_processed(message):
        return

      qty_last_fill = float(message['tradeVolume'])
      price_last_fill = float(message['tradePrice'])
      fill_info = self._update_fill_info(order, price_last_fill, qty_last_fill, is_rest=False)
      last_fill = order.internal.fill.update_by_accum_fill(
          fill_info.value.price_avg_fill,
          fill_info.value.qty_accum_fill,
      )
      agg = message.get('aggressor')
      if not agg:
        fill_type = coin_order_pb2.MAKER_FILL_TYPE
      else:
        fill_type = coin_order_pb2.TAKER_FILL_TYPE
      self._process_last_fill(last_fill, order, fill_type, timestamp)

      state = HuobiOrderState(message['orderStatus'])
      if state == HuobiOrderState.FILLED:
        if not self._order_manager.is_dead_order(order):
          self._order_manager.set_order(order, fully_filled=True)
    elif event_type == 'cancellation':
      # Process cancels
      exchange_order_id = str(message['orderId'])

      order = self._order_manager.get(exchange_order_id=exchange_order_id)
      if order is None:
        self._logger.error('Ws cancel can not find order! %s', exchange_order_id)
        return
      if not self._order_manager.is_dead_order(order):
        self._order_manager.set_order(order, cancel_confirmed=True)
        self._og_logger.gen_order_event_and_log(order, timestamp, OrderEvent.CANCEL_CONFIRMED)
    else:
      self._logger.error('Unknown ws order update! %s', json.dumps(message, indent=2))

  @exchange_message_handler(_MsgType.ORDER_DETAIL)
  def _handle_order_detail(self, exchange_message: PrivateExchangeMessage):
    """
    {
    "status": "ok",
    "data": {
    "id": 28478169694,
    "symbol": "btcusdt",
    "account-id": 4836027,
    "amount": "0.340000000000000000",
    "price": "4929.940000000000000000",
    "created-at": 1554286093888,
    "type": "buy-limit-maker",
    "field-amount": "0.0",
    "field-cash-amount": "0.0",
    "field-fees": "0.0",
    "finished-at": 0,
    "source": "spot-api",
    "state": "submitted",
    "canceled-at": 1554286094124
    }
    }
    """
    timestamp = exchange_message.received_timestamp
    response = exchange_message.data
    message = response.json()
    if message['status'] != 'ok':
      self._logger.error('status is not ok: %s' % message)
      return

    data = message['data']
    data['filled-amount'] = data['field-amount']
    data['filled-cash-amount'] = data['field-cash-amount']
    data['filled-fees'] = data['field-fees']
    data['extra_info'] = 'order_detail'

    order_info = create_order_from_open_order_message(data, timestamp)
    order = self._order_manager.get(exchange_order_id=order_info.internal.exchange_order_id)
    if order is None:
      self._logger.error('order not found! %s', message)
      return

    status = order_info.internal.status
    if status in (HuobiOrderState.CANCELED, HuobiOrderState.PARTIAL_CANCELED):
      if not self._order_manager.is_dead_order(order):
        self._order_manager.set_order(order, cancel_confirmed=True)
        self._og_logger.gen_order_event_and_log(order, timestamp, OrderEvent.CANCEL_CONFIRMED)
    elif status == HuobiOrderState.FILLED:
      if not self._order_manager.is_dead_order(order):
        self._order_manager.set_order(order, fully_filled=True)
    else:
      if status != HuobiOrderState.CANCELLING:
        self._logger.error('order not in canceling state! %s %s', order.order_id, status)
